/**
 * FileFind - ftp/shares indexing and search php engine
 * Module: agent-java
 * Description: Agent written in java language
 * Copyright (c) 2003-2010 Rage Steel, www.gt2.ru
 */
package ru.ffind.agent.sync;

import java.util.Collection;
import java.util.Map;

import ru.ffind.common.model.Directory;
import ru.ffind.common.model.Entry;
import ru.ffind.common.model.Filename;
import ru.ffind.agent.model.ListingStorage;
import ru.ffind.agent.model.RootDirectory;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

/**
 * Database syncronization
 *
 * @author Rage Steel
 */
public class Sync {

	private final ListingStorage sourceStorage;
	private final ListingStorage targetStorage;
	private final SyncActions syncActions;
	
	/**
     * Create a new syncronization
	 * 
	 * @param sourceStorage reference directory (read by files)
	 * @param targetStorage directory structure in the database
     * @param syncActions actions to use to modify database.
	 */
	public Sync(ListingStorage sourceStorage, ListingStorage targetStorage, SyncActions syncActions) {
		super();
		this.sourceStorage = sourceStorage;
		this.targetStorage = targetStorage;
		this.syncActions = syncActions;
	}

	/**
	 * Start synchronization
	 */
	public void sync() {
		doSync(RootDirectory.ROOT_NAME, null);
	}
	
	public static final long RETRY_TIMEOUT = 1000;
	
	private static Directory getDirectoryWithWait(String absolutePath,
            ListingStorage listingStorage) {
		return listingStorage.getAndRemoveWithWait(absolutePath, RETRY_TIMEOUT);
	}
	
	private Directory getSource(String absolutePath) {
		return getDirectoryWithWait(absolutePath, sourceStorage);
	}

	private Directory getTarget(String absolutePath) {
		return getDirectoryWithWait(absolutePath, targetStorage);
	}

	/**
	 * Start synchronization for given absolutePath
	 *  
	 * @param absolutePath
	 */
	private void doSync(final String absolutePath, Directory parentDirectory) {
		final Directory source = getSource(absolutePath);
		final Directory target = getTarget(absolutePath);
		
		if ((null == source) || source.isError()) {
			if (null != target) {
				syncActions.removeDir(absolutePath, target);
				return;
			}
		}
		
		if (null == target) {
//			System.out.println("CopyDir: " + absolutePath);
			syncActions.createDir(absolutePath, parentDirectory, source);
			return;
		}

//		System.out.println("We need a sync routing here");
		
		final ImmutableList<Entry> sourceEntries = source.getEntries();
		final ImmutableList<Entry> targetEntries = target.getEntries();
		
		final Collection<Filename> createFiles = Lists.newArrayList();
		final Collection<Filename> removeFiles = Lists.newArrayList();
		final Map<Filename, Filename> updateFiles = Maps.newHashMap();
		
		abstract class ProcessEntries {
			abstract protected void onFile(Filename filename);
			
			abstract protected void onDir(Directory dir);
			
			protected void onEntry(Entry entry) {
				if (entry instanceof Filename) {
					onFile((Filename)entry);
				} else if (entry instanceof Directory) {
					onDir((Directory)entry);
				} else {
					throw new RuntimeException("Unknown entry class: " + entry.getClass());
				}
			}
			
			public void process(Collection<Entry> entries) {
				for (Entry entry: entries) {
					onEntry(entry);
				}
			}
		}
		
		// Entries that exists in the source but not in the target
		Collection<Entry> createEntries = Lists.newArrayList(sourceEntries);
		if (createEntries.removeAll(targetEntries)) {
			// Create this entries
			new ProcessEntries() {
				
				@Override
				protected void onFile(Filename filename) {
					createFiles.add(filename);
				}
				
				@Override
				protected void onDir(Directory dir) {
					syncActions.createDir(dir.getAbsPath(absolutePath), target, dir);
				}
			}.process(createEntries);
		}

		// Entries that exists in the target but not in the source
		Collection<Entry> removeEntries = Lists.newArrayList(targetEntries);
		if (removeEntries.removeAll(sourceEntries)) {
			// Remove this entries
			new ProcessEntries() {
				
				@Override
				protected void onFile(Filename filename) {
					removeFiles.add(filename);
				}
				
				@Override
				protected void onDir(Directory dir) {
					syncActions.removeDir(dir.getAbsPath(absolutePath), target);
				}
			}.process(removeEntries);
		}

		// Entries that exists in the target and in the source
		Collection<Entry> commonEntriesSource = Lists.newArrayList(sourceEntries);
		commonEntriesSource.retainAll(targetEntries);
		if (!commonEntriesSource.isEmpty()) {
			new ProcessEntries() {
				
				Entry targetEntry;
				
				@Override
				protected void onEntry(Entry entry) {
					int targetIndex = targetEntries.indexOf(entry);
					if (-1 == targetIndex) {
						throw new RuntimeException("Target index not found for common entries list, entry: " + entry);
					}
					targetEntry = targetEntries.get(targetIndex);
					
					super.onEntry(entry);
				};
				
				@Override
				protected void onFile(Filename filename) {
					if (targetEntry instanceof Directory) {
						Directory targetDir = (Directory)targetEntry;
						// Remove target directory
						syncActions.removeDir(targetDir.getAbsPath(absolutePath), targetDir);
						// And add file
						createFiles.add(filename);
					} else {
						// Update metadata if needed
						if (!filename.equalsMetadata(targetEntry)) {
							updateFiles.put(filename, (Filename)targetEntry);
						}
					}
				}
				
				@Override
				protected void onDir(Directory dir) {
					if (targetEntry instanceof Filename) {
						// Remove target file
						removeFiles.add((Filename)targetEntry);
						// And add directory
						syncActions.createDir(dir.getAbsPath(absolutePath), target, dir);
					} else {
						// Update metadata if needed
						if (dir.equalsMetadata(targetEntry)) {
							doSync(dir.getAbsPath(absolutePath), target);
							syncActions.editDir((Directory)targetEntry, dir);
						}
					}
				}
			}.process(commonEntriesSource);
		}
		
		syncActions.createFiles(createFiles, target);
		syncActions.removeFiles(removeFiles);
		syncActions.editFiles(updateFiles);
		
		// FIXME Update directory metadata
	}
}
